﻿using GameToolkit;
using ICSharpCode.SharpZipLib.Zip;
using LitJson;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEngine;

namespace Devil.Persistent
{
    public partial class PersistentSystem
    {
        int GetAllBytes(ZipInputStream zip, ref byte[] allBytes, byte[] buffer)
        {
            int length = 0;
            int len;
            while ((len = zip.Read(buffer, 0, buffer.Length)) > 0)
            {
                var size = length + len;
                NativeArrayUtil.EnsureNativeSize(ref allBytes, length, size);
                System.Array.Copy(buffer, 0, allBytes, length, len);
                length += len;
            }
            return length;
        }

        public bool ParseMetaFile<T>(string filePath, T meta) where T : IJsonSerializable
        {
            if (meta == null || string.IsNullOrEmpty(filePath) || !NativeFileSystem.HasFile(filePath))
                return false;
            Stream fin = null;
            bool result = false;
            try
            {
                fin = NativeFileSystem.OpenFile(filePath, FileMode.Open, FileAccess.Read);
                var zip = new ZipInputStream(fin);
                if (!string.IsNullOrEmpty(mPwd))
                    zip.Password = mPwd;
                ZipEntry entry;
                byte[] allBytes = null;
                byte[] buffer = new byte[mBufferSize];
                int length = 0;
                while ((entry = zip.GetNextEntry()) != null)
                {
                    if (entry.IsFile && entry.Name.EqualsIgnoreCase("meta"))
                    {
                        length = GetAllBytes(zip, ref allBytes, buffer);
                        break;
                    }
                }
                if (length > 0)
                {
                    var txt = Encoding.UTF8.GetString(allBytes, 0, length);
                    var json = JsonMapper.ToObject(txt);
                    meta.OnDeserialize(json);
                    result = true;
                }
                zip.Dispose();
            }
            catch (System.Exception e)
            {
                Debug.LogException(e);
            }
            finally
            {
                if (fin != null)
                    fin.Dispose();
            }
            return result;
        }

        struct ReadInstance
        {
            public IPersistentBlock block;
            public object instance;
            public JsonData content;
        }

        class FileBuffer
        {
            internal string extension;
            internal readonly int id;
            internal readonly byte[] allBytes;
            internal FileBuffer(int id, byte[] bytes)
            {
                this.id = id;
                this.allBytes = bytes;
            }
        }

        class ReadDataBlock
        {
            internal int version;
            internal readonly IPersistentBlock block;
            internal readonly List<ReadInstance> datas;
            internal readonly List<ReadInstance> references;
            internal string deserializeJson;
            internal List<IBeforeDeserializeScheduler> beforeCmds;
            //internal List<IAfterDeserializeScheduler> afterCmds;
            internal bool hasBinaryFile;
            internal ReadDataBlock(IPersistentBlock block)
            {
                this.block = block;
                datas = new List<ReadInstance>();
                references = new List<ReadInstance>();
            }

            internal List<IBeforeDeserializeScheduler> EnsureBeforeCmds()
            {
                if (beforeCmds == null)
                    beforeCmds = new List<IBeforeDeserializeScheduler>();
                return beforeCmds;
            }

            internal object GetInstance(int instanceId)
            {
                for (int k = datas.Count - 1; k >= 0; k--)
                {
                    if (datas[k].instance is ISharedInstance inst && inst.InstanceId == instanceId)
                        return inst;
                }
                for (int k = references.Count - 1; k >= 0; k--)
                {
                    if (references[k].instance is ISharedInstance inst && inst.InstanceId == instanceId)
                        return inst;
                }
                return null;
            }

            //internal List<IAfterDeserializeScheduler> EnsureAfterCmds()
            //{
            //    if (afterCmds == null)
            //        afterCmds = new List<IAfterDeserializeScheduler>();
            //    return afterCmds;
            //}
        }

        class ReadGameCmd : RwCmd, IYieldCommand, IBreakCommandSignal
        {
            PersistentSystem mSys;
            IPersistentMeta mMeta;
            string mFilePath;
            List<ReadDataBlock> mBlocks;
            Dictionary<int, FileBuffer> mBinaryFiles;
            List<IBeforeDeserializeScheduler> mBeforeCmds;
            //List<IAfterDeserializeScheduler> mAfterCmds;
            bool mHasError;

            internal ReadGameCmd(PersistentSystem sys, IPersistentMeta meta, string filePath)
            {
                mSys = sys;
                mMeta = meta;
                mFilePath = filePath;
                mBlocks = new List<ReadDataBlock>();
                mBeforeCmds = new List<IBeforeDeserializeScheduler>();
                mBinaryFiles = new Dictionary<int, FileBuffer>();
                //mAfterCmds = new List<IAfterDeserializeScheduler>();
            }

            public override string ToString()
            {
                if(mMeta == null || mMeta.BlockType == EPersistentType.System)
                    return $"Read System @{mFilePath}";
                else
                    return $"Read Game @{mFilePath}";
            }

            internal override IPersistentBlock GetBlock(object instance)
            {
                for (int i = mBlocks.Count - 1; i >= 0; i--)
                {
                    if (mBlocks[i].block.IsInstanceSerializer(instance))
                        return mBlocks[i].block;
                }
                return null;
            }

            internal override object GetInstance(string blockName, int instanceId)
            {
                for (int i = mBlocks.Count - 1; i >= 0; i--)
                {
                    var block = mBlocks[i];
                    if (blockName.EqualsIgnoreCase(block.block.BlockName))
                    {
                        return block.GetInstance(instanceId);
                    }
                }
                return null;
            }

            void IBreakCommandSignal.OnBreak()
            {
                mHasError = true;
                lock (mSys.mRwCmds)
                {
                    mSys.mRwCmds.Remove(this);
                }
                lock (_singletonLock)
                {
                    if (mMeta == null || mMeta.BlockType == EPersistentType.System)
                    {
                        mSys.mSystemJob = ParallelUtils.Schedule(NotifyReadResult, true, mSys.mSystemJob);
                    }
                    else
                    {
                        mSys.mGameJob = ParallelUtils.Schedule(NotifyReadResult, true, mSys.mGameJob);
                    }
                }
            }

            IEnumerator IYieldCommand.Execute()
            {
                var blockType = mMeta == null ? EPersistentType.System : mMeta.BlockType;
                foreach (var mod in mSys.mModules)
                {
                    if (mod is IPersistentBlock block && block.BlockType == blockType)
                    {
                        mBlocks.Add(new ReadDataBlock(block));
                    }
                    if (mod is IBeforeDeserializeScheduler cmd1)
                        mBeforeCmds.Add(cmd1);
                    //if (mod is IAfterDeserializeScheduler cmd2)
                    //    mAfterCmds.Add(cmd2);
                }
                mHasError |= !ReadFile();
                if (!mHasError)
                {
                    var job = ParallelUtils.ScheduleParallel<ReadDataBlock>(DeserializeBlockData, mBlocks);
                    yield return job;
                    lock (mSys.mRwCmds)
                    {
                        if (!mSys.mRwCmds.Contains(this))
                            mSys.mRwCmds.Add(this);
                    }
                    var beforeCmds = ParallelUtils.NewCombination($"Before Deserialize({blockType})");
                    if (mBeforeCmds.Count > 0)
                    {
                        for (int i = 0; i < mBeforeCmds.Count; i++)
                        {
                            beforeCmds.Combine(mBeforeCmds[i].Schedule(mMeta));
                        }
                    }
                    for (int i = 0; i < mBlocks.Count; i++)
                    {
                        var block = mBlocks[i];
                        if (block.beforeCmds != null)
                        {
                            for (int k = 0; k < block.beforeCmds.Count; k++)
                            {
                                beforeCmds.Combine(block.beforeCmds[k].Schedule(mMeta));
                            }
                        }
                    }
                    if (beforeCmds.IsAlive)
                        yield return beforeCmds;
                    var jobs = ParallelUtils.NewCombination("Deserialize Persistent Data");
                    System.Action<ReadInstance> deserializer = DeserializeInstance;
                    System.Action<ReadInstance> binser = DeserializeBinaryFile;
                    for (int i = 0; i < mBlocks.Count; i++)
                    {
                        var block = mBlocks[i];
                        if (block.datas.Count > 0)
                        {
                            jobs.Combine(ParallelUtils.ScheduleParallel(deserializer, block.datas));
                            if (block.hasBinaryFile)
                                jobs.Combine(ParallelUtils.ScheduleParallel(binser, block.datas));
                        }
                        if (block.references.Count > 0)
                        {
                            jobs.Combine(ParallelUtils.ScheduleParallel(deserializer, block.references));
                            if (block.hasBinaryFile)
                                jobs.Combine(ParallelUtils.ScheduleParallel(binser, block.references));
                        }
                    }
                    yield return jobs;
                    lock (mSys.mRwCmds)
                    {
                        mSys.mRwCmds.Remove(this);
                    }
                    //var afterCmds = ParallelUtils.NewCombination($"After Deserialize({blockType})");
                    //for (int i = 0; i < mBlocks.Count; i++)
                    //{
                    //    var block = mBlocks[i];
                    //    if (block.afterCmds != null)
                    //    {
                    //        for (int k = 0; k < block.afterCmds.Count; k++)
                    //        {
                    //            afterCmds.Combine(block.afterCmds[k].Schedule(mMeta));
                    //        }
                    //    }
                    //}
                    //if (mAfterCmds.Count > 0)
                    //{
                    //    for (int i = 0; i < mAfterCmds.Count; i++)
                    //    {
                    //        afterCmds.Combine(mAfterCmds[i].Schedule(mMeta));
                    //    }
                    //}
                    //if (afterCmds.IsAlive)
                    //    yield return afterCmds;
                    var restore = ParallelUtils.ScheduleParallel<ReadDataBlock>(RestoreBlockData, mBlocks);
                    yield return restore;
                }
                else
                {
                    var restore = ParallelUtils.ScheduleParallel<ReadDataBlock>(RestoreBlockData, mBlocks);
                    yield return restore;
                }

                yield return ParallelUtils.Schedule(NotifyReadResult, true);
            }

            void NotifyReadResult()
            {
                mSys.mOnLoaded?.Invoke(mMeta, !mHasError);
            }

            ReadDataBlock GetBlock(string name)
            {
                for (int i = 0; i < mBlocks.Count; i++)
                {
                    var block = mBlocks[i];
                    if (block.block.BlockName.EqualsIgnoreCase(name))
                        return block;
                }
                return null;
            }

            void DeserializeArray(ReadDataBlock block, JsonData array, ICollection<ReadInstance> instances)
            {
                var count = array == null ? 0 : array.Count;
                for (int i = 0; i < count; i++)
                {
                    var content = array[i];
                    try
                    {
                        if (block.version < block.block.Version && block.block is IUpgradablePersistentBlock upgrade)
                            content = upgrade.Upgrade(content);
                        var inst = block.block.CreateInstance(content);
                        if (inst != null)
                        {
                            ReadInstance dt;
                            dt.block = block.block;
                            dt.instance = inst;
                            dt.content = content;
                            instances.Add(dt);
                            if (inst is IBeforeDeserializeScheduler bef)
                                block.EnsureBeforeCmds().Add(bef);
                            block.hasBinaryFile |= inst is IBinaryPersistentFile;
                            //if (inst is IAfterDeserializeScheduler after)
                            //    block.EnsureAfterCmds().Add(after);
                        }
                    }
                    catch (System.Exception e)
                    {
                        Debug.LogException(e);
                    }
                }
            }

            void DeserializeBlockData(ReadDataBlock block)
            {
                var json = string.IsNullOrEmpty(block.deserializeJson) ? null : JsonMapper.ToObject(block.deserializeJson);
                if (json != null)
                {
                    block.version = (int)json["version"];
                    var serializedDatas = json["data"];
                    var serializedRefs = json["refs"];

                    DeserializeArray(block, serializedDatas, block.datas);
                    DeserializeArray(block, serializedRefs, block.references);
                }
            }

            void RestoreBlockData(ReadDataBlock block)
            {
                if (block.block is IRestorablePersistentBlock restore)
                {
                    var lst = new List<object>(block.datas.Count);
                    var refs = new List<object>(block.references.Count);
                    for (int i = 0; i < block.datas.Count; i++)
                    {
                        lst.Add(block.datas[i].instance);
                    }
                    for (int i = 0; i < block.references.Count; i++)
                    {
                        refs.Add(block.references[i].instance);
                    }
                    restore.RestorePersistentDatas(lst, refs);
                }
            }

            void DeserializeInstance(ReadInstance inst)
            {
                try
                {
                    inst.block.OnDeserializeParallel(inst.instance, inst.content);
                }
                catch (System.Exception e)
                {
                    Debug.LogException(e);
                }
            }

            void DeserializeBinaryFile(ReadInstance inst)
            {
                try
                {
                    FileBuffer buffer;
                    if (inst.instance is IBinaryPersistentFile file && mBinaryFiles.TryGetValue(file.InstanceId, out buffer))
                    {
                        var ext = file.FileType;
                        if (!string.IsNullOrEmpty(ext) && ext[0] == '.')
                            ext = ext.Substring(1);
                        if (ext.EqualsIgnoreCase(buffer.extension))
                            file.LoadBytes(buffer.allBytes, 0, buffer.allBytes.Length);
                        else
                            RTLog.LogError(LogCat.Persistent, $"miss match binary file type. (file: {buffer.extension}, inst: {inst.instance.GetType().Name})");
                    }
                }
                catch (System.Exception e)
                {
                    Debug.LogException(e);
                }
            }

            bool ReadFile()
            {
                var result = false;
                Stream fin = null;
                try
                {
                    fin = NativeFileSystem.OpenFile(mFilePath, FileMode.Open, FileAccess.Read);
                    var zip = new ZipInputStream(fin);
                    if (!string.IsNullOrEmpty(mSys.mPwd))
                        zip.Password = mSys.mPwd;
                    ZipEntry entry;
                    byte[] allBytes = null;
                    var buffer = new byte[mSys.mBufferSize];
                    while ((entry = zip.GetNextEntry()) != null)
                    {
                        if (!entry.IsFile)
                            continue;
                        if (entry.Name.StartsWithIgnoreCase("DT_"))
                        {
                            var blockName = entry.Name.Substring(3);
                            var block = GetBlock(blockName);
                            if (block == null)
                                continue;
                            var len = mSys.GetAllBytes(zip, ref allBytes, buffer);
                            block.deserializeJson = Encoding.UTF8.GetString(allBytes, 0, len);
                        }
                        else if (entry.Name.StartsWithIgnoreCase("bin/"))
                        {
                            int id;
                            var fname = NativeFileSystem.GetFileName(entry.Name, true);
                            if (Localization.TryParse(fname, out id, System.Globalization.NumberStyles.HexNumber))
                            {
                                var len = mSys.GetAllBytes(zip, ref allBytes, buffer);
                                if (len > 0)
                                {
                                    var bytes = new byte[len];
                                    System.Array.Copy(allBytes, bytes, len);
                                    var file = new FileBuffer(id, bytes);
                                    var pos = entry.Name.LastIndexOf('.');
                                    file.extension = pos > 0 ? entry.Name.Substring(pos + 1) : null;
                                    mBinaryFiles[id] = file;
                                }
                            }
                        }
                    }
                    zip.Dispose();
                    result = true;
                }
                catch (System.Exception e)
                {
                    Debug.LogException(e);
                }
                finally
                {
                    if (fin != null)
                        fin.Dispose();
                }
                return result;
            }

        }
    }
}
